package com.ringtheweb.serviceprovider.weather.model;

import static javax.persistence.GenerationType.AUTO;

import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.hibernate.annotations.AccessType;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Index;

import com.ringtheweb.serviceprovider.weather.exception.IllegalLocationException;


@Entity
@AccessType("field")
@Table(name="LOCATIONS",
		uniqueConstraints={@UniqueConstraint(columnNames={"PARENT_LOCATION_ID","LOCATION_NAME","LOCURL"})}
		)
@org.hibernate.annotations.Table(appliesTo="LOCATIONS",indexes={
		@Index(name="name_idx",columnNames={"LOCATION_NAME"}),
		@Index(name="parent_idx",columnNames={"PARENT_LOCATION_ID"})
		})
@NamedQueries({
    @NamedQuery(name="findLocationWithChildren",
        query="SELECT l FROM Location l inner join fetch l.children WHERE l.id = :locationId"),
    @NamedQuery(name="findLocationByNameAndParent",
        query="SELECT l FROM Location l WHERE l.name=:locationName and (" +
        		"l.parentLocation.id=:parentLocationId or l.parentLocation.id is null)"),
    @NamedQuery(name="findLocationByNameAndURLWithChildren",
        query="SELECT l FROM Location l inner join fetch l.children as c WHERE l.name=:locationName and l.url=:url" ),
    @NamedQuery(name="findLocationByNameAndParentName",
                query="SELECT l FROM Location l inner join fetch l.parentLocation as p WHERE (l.name=:name and l.locationType=:locationType) " +
                		"and p.name=:parentName"),
    @NamedQuery(name="findLocationByNameAndParentWithChildren",
        query="SELECT l FROM Location l inner join fetch l.children WHERE l.name=:locationName and (" +
        	    	                		"l.parentLocation.id=:parentLocationId or l.parentLocation.id is null)")      		
})
// Region is not used...only default one is used with JBoss Tree cache TODO: find out why 
@Cache(region="/Locations",usage=CacheConcurrencyStrategy.READ_WRITE)
    		 

@XmlRootElement
public class Location implements Serializable,Comparable<Location> {
	
	/** serialVersionUID */
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = AUTO)
	private Long id;
	
    @OneToMany(mappedBy = "parentLocation", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @org.hibernate.annotations.OrderBy(clause = "LOCATION_NAME asc")
	private List<Location> children;
	
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PARENT_LOCATION_ID", nullable = true)
    @org.hibernate.annotations.ForeignKey(name = "FK_LOCATION_PARENT_ID")	
	private Location parentLocation;
	
	@Column(name="LOCATION_NAME")
	private String name;
	
	@Column(name="LOCATION_TYPE",nullable=false)
	private LocationType locationType;
	
	@Column(name="LOCLEV",nullable=false)
	private int level = 0;
	
	@Column(name="COMPLETED")
	private boolean completed=false;
	
	@Column(name="LOCURL")
	private String url;
	
	
	public Location() {};
	
	public Location(String name,LocationType locationType) {
		this(name,locationType,null);
	}
	
	
	public Location(String name,LocationType locationType,URL url) {
		this.name=name;
		this.locationType = locationType;
		this.url = url.toExternalForm();
	}
	
	
	@XmlElement
	public LocationType getLocationType() {
		return locationType;
	}

	public void setLocationType(LocationType locationType) {
		this.locationType = locationType;
	}


	@XmlTransient
	public Location getParentLocation() {
		return parentLocation;
	}

	public void setParentLocation(Location parentLocation) {
		this.parentLocation = parentLocation;
	}

	
	@XmlElement
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}


	@XmlTransient
	public int getLevel() {
		return level;
	}

	public void setLevel(int level) {
		this.level = level;
	}	
	
	@XmlElement
	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	@XmlTransient
	public boolean isCompleted() {
		return completed;
	}

	public void setCompleted(boolean completed) {
		this.completed = completed;
	}

	public void setId(Long id) {
		this.id = id;
	}

	@XmlElement
	public Long getId() {
		return id;
	}

	@XmlTransient
	public List<Location> getChildren() {
		return children;
	}
	
    public int getNumberOfChildren() {
        if (children == null) {
            return 0;
        }
        return children.size();
    }

	public void setChildren(List<Location> children) {
		this.children = children;
	}
	
	/**
	 * Add a child location to the current location
	 * @param child
	 * @throws IllegalLocationException
	 */
    public void addChildLocation(Location child) {
    	if (child == this) {
    		throw new IllegalLocationException("Trying to add this as child");
    	}
        if (children == null) {
            children = new ArrayList<Location>();
        }
        children.add(child);
        child.setParentLocation(this);
        child.setLevel(getLevel()+1);
    }
    
    
    public void addChildrenLocations(Collection<Location> locations) {
    	for (Location location : locations) {
    		addChildLocation(location);
    	}
    }
    
    /**
     * Inserts a Node<T> at the specified position in the child list. Will     * throw an ArrayIndexOutOfBoundsException if the index does not exist.
     * @param index the position to insert at.
     * @param child the Node<T> object to insert.
     * @throws IndexOutOfBoundsException if thrown.
     */
    public void insertChildLocationAt(int index, Location child) throws IndexOutOfBoundsException {
        if (index == getNumberOfChildren()) {
            // this is really an append
            addChildLocation(child);
            return;
        } else {
            children.get(index); //just to throw the exception, and stop here
            children.add(index, child);
        }
    }


    /**
     * Remove the Node<T> element at index index of the List<Node<T>>.
     * @param index the index of the element to delete.
     * @throws IndexOutOfBoundsException if thrown.
     */
    public void removeChildLocationAt(int index) throws IndexOutOfBoundsException {
        children.remove(index);
    }


    @Override
	public boolean equals(Object obj) {
		
		if (obj == this) {
			   return true;
		}
		if (!(obj instanceof Location)) {
			return false;
		}
	
		Location location=(Location)obj;
		
	    if (getId()!= null && location.getId()!=null) {
	    	return getId().equals(location.getId());
	    }
		
		boolean isEqual =  (this.name == null? location.getName() == null :
				this.name.equals(location.getName()));
		
		if (isEqual) {
			isEqual = (this.url == null ? location.getUrl() == null : 
				this.url.equals(location.getUrl()));
		}
		if (isEqual) {
			isEqual = (this.parentLocation == null ? location.getParentLocation() == null :
				this.parentLocation.equals(location.getParentLocation()));
		}
	    return isEqual;
	
	}

	@Override
	public String toString() {
		return new ToStringBuilder(this)
			.append("name", getName())
			.append("location",getLocationType())
			.append("level",getLevel())
			.append("url",getUrl()==null?"null":getUrl())
			.toString();
		
	}

	public int compareTo(Location location) {
	    int result =  this.getName().compareTo( location.getName() );
	    if (null != url && result == 0 ) {
	    	result = this.getUrl().compareTo(location.getUrl());
	    }
	    if (null != parentLocation && result == 0){
	    	result = this.getParentLocation().compareTo(location.getParentLocation());
	    }
	    return result;
	}

	@Override
	public int hashCode() {
	    int result = 17;
	    result = result + (name == null ? 0 : name.hashCode());
	    result = result + (url == null ? 0 : url.hashCode());
	    result = result + (parentLocation == null ? 0 : parentLocation.hashCode());
	    return result;
	}	

}
